#!/usr/bin/env python
# -*- coding: utf-8 -*-

#
# This program is free software; you can redistribute it and/or modify  
# it under the terms of the GNU General Public License as published by  
# the Free Software Foundation; either version 2 of the License, or     
# (at your option) any later version.                                   
#                                                                         
# A copy of the license can be found in the license.txt file supplied   
# with this software or at: http://www.gnu.org/copyleft/gpl.html       
#


import time,socket,base64
import Queue as queue

import celtuce.net.ioloop as ioloop
import celtuce.net.iostream as iostream

import celtuce.help.logging as logging
import celtuce.help.url as urlparser
import celtuce.help.codec as codec

from celtuce.helper import getTimestamp
from celtuce.event import Event,ROUTE_GET,ROUTE_DIGEST,ROUTE_DEMAND,OP_RESPONSE,OP_FAILURE
from celtuce.event import OP_INSERT,OP_ERASE,OP_FIND
from celtuce.response import Response, RC_TIMEOUT,RC_INTERNAL_ERROR,RC_TABLET_SERVER_NOT_FOUND

from accesshelp import MetaAccessor,DataAccessor

from bisect import bisect_left,insort


class   Accessor(object) :
    def __init__(self, meta_url) :
        self._meta_accessor = MetaAccessor( meta_url )
        self._meta_accessor.start()

        self._data_accessor = DataAccessor()

        self._id_base = id(self)
        self._id_serial = 1
        self._id_maximum = 1024 * 1024 * 1024

        self._meta_queue = queue.Queue()
        self._TIMEOUT_WAIT_META_SERVER = 10

        self._data_queue = queue.Queue()
        self._TIMEOUT_WAIT_DATA_SERVER = 10

        self._route_tablet = {}
        self._route_tablet_text = None
        self._route_tablet_digest = None
        self._route_last_update = 0

        self._logger = logging.getLogger('celtuce')
        ## __init__()

    def __del__(self) :
        self.shutdown()
        ## __del__()

    def shutdown(self) :
        self._logger.info('Accessor is shutdowning ...')
        self._meta_accessor.stop()
        if self._meta_accessor.isAlive() :
            self._meta_accessor.join()

        self._data_accessor.shutdown()
        ## shutdown()

    ####-----------------------------------------------------------####

    def insert(self, table_name, key, data, timeout_ms=-1) :
        response = Response()

        # get current timestamp
        timestamp_ms = getTimestamp()

        # STEP. lookup tablet-servers
        tablet_servers = self.lookupTabletServer(table_name, key, timeout_ms < 1)
        if not tablet_servers :
            response.status_code = RC_TABLET_SERVER_NOT_FOUND
            response.error = 'No tablet-server for %s(key:%s)' % (table_name, repr(key))
            return response

        # STEP. TODO to check if there is enough tablet-servers 

        # STEP. make INSERT request
        request = Event()
        request.op = OP_INSERT
        request.body = (table_name, key, data, timestamp_ms)
        request.request_id = self.genRequestID()

        # STEP. send request to tablet-servers concurrently, and wait response
        replies = self.__callDataServers(request, tablet_servers)

        # STEP. check response; update route-table if necessary
        i = 0
        for reply in replies :
            i += 1
            self._logger.debug('#%d reply : %s', i, reply)   

        # STEP. make up response

        return response
        ## insert()

    def erase(self, table_name, key, timeout_ms=-1) :
        response = Response()

        # get current timestamp
        timestamp_ms = getTimestamp()

        # STEP. lookup tablet-servers
        tablet_servers = self.lookupTabletServer(table_name, key, timeout_ms < 1)
        if not tablet_servers :
            response.status_code = RC_TABLET_SERVER_NOT_FOUND
            response.error = 'No tablet-server for %s(key:%s)' % (table_name, repr(key))
            return response

        # STEP. TODO to check if there is enough tablet-servers 

        # STEP. make ERASE request
        request = Event()
        request.op = OP_ERASE
        request.body = (table_name, key, timestamp_ms)
        request.request_id = self.genRequestID()

        # STEP. send request to tablet-servers concurrently, and wait response
        replies = self.__callDataServers(request, tablet_servers)

        # STEP. check response; update route-table if necessary
        i = 0
        for reply_from,reply in replies :
            i += 1
            if reply.isOk() :
                self._logger.debug('#%d reply (from %s) body : %s', i, reply_from, reply.body)   
            else :
                self._logger.debug('#%d reply (from %s) error : %s', i, reply_from, reply.error)   

        # STEP. make up response

        return response
        ## erase()

    def find(self, table_name, key, timeout_ms=-1) :
        '''
        @param table_name name of table
        @param key key of record
        @param timeout_ms timeout of find operation (in milliseconds)

        @return 2-tuple (key, value | None)
        '''
        response = Response()

        # get current timestamp
        timestamp_ms = getTimestamp()

        # STEP. lookup tablet-servers
        tablet_servers = self.lookupTabletServer(table_name, key, timeout_ms < 1)
        if not tablet_servers :
            response.status_code = RC_TABLET_SERVER_NOT_FOUND
            response.error = 'No tablet-server for %s(key:%s)' % (table_name, repr(key))
            return response

        # STEP. TODO to check if there is enough tablet-servers 

        # STEP. make FIND request
        request = Event()
        request.op = OP_FIND
        request.body = (table_name, key, timestamp_ms)
        request.request_id = self.genRequestID()

        # STEP. send request to tablet-servers concurrently, and wait response
        replies = self.__callDataServers(request, tablet_servers)

        # STEP. check response; update route-table if necessary
        unique_dataset = []
        i = 0
        for reply_from,reply in replies :
            i += 1
            if reply.isOk() :
                self._logger.debug('#%d reply (from %s) body : %s', i, reply_from, reply.body)   
                self.__mergeRowSet(unique_dataset, reply.body)
            else :
                self._logger.debug('#%d reply (from %s) error : %s', i, reply_from, reply.error)   

        # STEP. merge found data, and return the last one
        if unique_dataset :
            row = unique_dataset[ -1 ]
            data = row[2]
        else :
            data = None
            
        # STEP. TODO : to do some read-repair

        # STEP. make up response
        if data :
            response.body = (key, data)
        else :
            response.body = (key, None)

        return response
        ## find()

    def findall(self, table_name, key, timeout_ms=-1) :
        '''
        @param table_name name of table
        @param key key of record
        @param timeout_ms timeout of find operation (in milliseconds)

        @return 3-tuple (key, count-of-values, list-of-values | None)
        '''
        response = Response()

        # get current timestamp
        timestamp_ms = getTimestamp()

        # STEP. lookup tablet-servers
        tablet_servers = self.lookupTabletServer(table_name, key, timeout_ms < 1)
        if not tablet_servers :
            response.status_code = RC_TABLET_SERVER_NOT_FOUND
            response.error = 'No tablet-server for %s(key:%s)' % (table_name, repr(key))
            return response

        # STEP. TODO to check if there is enough tablet-servers 

        # STEP. make FIND request
        request = Event()
        request.op = OP_FIND
        request.body = (table_name, key, timestamp_ms)
        request.request_id = self.genRequestID()

        # STEP. send request to tablet-servers concurrently, and wait response
        replies = self.__callDataServers(request, tablet_servers)

        # STEP. check response; update route-table if necessary
        unique_dataset = []
        i = 0
        for reply_from,reply in replies :
            i += 1
            if reply.isOk() :
                self._logger.debug('#%d reply (from %s) body : %s', i, reply_from, reply.body)   
                self.__mergeRowSet(unique_dataset, reply.body)
            else :
                self._logger.debug('#%d reply (from %s) error : %s', i, reply_from, reply.error)   

        # STEP. merge found data
        #       make sure all data is not delete
        index_delete = len(unique_dataset) - 1
        while index_delete >= 0 :
            _op = unique_dataset[ index_delete ][ 1 ]
            if _op != 0 :
                # not insert
                break
            index_delete -= 1
        if index_delete >= 0 :
            unique_dataset = unique_dataset[ index_delete + 1 : ]
            
        dataset = []
        for ts,op,data in unique_dataset :
            dataset.append( data )

        # STEP. TODO : to do some read-repair

        # STEP. make up response
        response.body = [key, len(dataset), dataset]

        return response
        ## findall()


    def __mergeRowSet(self, unique_dataset, record) :
        if not record : 
            return

        try :
            key,op,ts,data = record
        except :
            return

        row = (ts, op, data)
        found = False

        index = bisect_left(unique_dataset, row)
        if index < len(unique_dataset) :
            _row = unique_dataset[ index ]
            _ts,_op = _row[0:2]
            if _ts == ts and _op == op :
                found = True

        if not found :
            insort(unique_dataset, row)
        ## __mergeRowSet()

    ####-----------------------------------------------------------####

    def needUpdateRouteTable(self) :
        '''
        @return True on success, False on failure
        '''
        request = Event()
        request.request_id = self.genRequestID()
        request.op = ROUTE_DIGEST

        response = self.__callMetaServer( request )
        if not response :
            self._logger.error('fail to check route table digest.')
            return False
        if not response.isOk() :
            self._logger.error('fail to check route table digest -- %s', response.error)
            return False

        route_tablet_digest = response.body
        if route_tablet_digest == self._route_tablet_digest :
            # the same digest, route table is updated
            return False

        return True
        ## needUpdateRouteTable()

    def updateRouteTable(self) :
        '''
        @return True on success, False on failure
        '''
        request = Event()
        request.request_id = self.genRequestID()
        request.op = ROUTE_GET

        response = self.__callMetaServer( request )
        if not response :
            self._logger.error('fail to update route table.')
            return False
        if not response.isOk() :
            self._logger.error('fail to update route table -- %s', response.error)
            return False

        # TODO : parse and load route table
        route_data = response.body
        self._route_tablet_text,self._route_tablet_digest = route_data
        self._route_last_update = getTimestamp()
        #self._logger.info('to update route table with %s', repr(route_data))

        self._route_tablet = codec.decode( base64.b64decode( self._route_tablet_text ) )
        self._logger.info('ROUTE tablet : %s', repr(self._route_tablet))

        return True
        ## updateRouteTable()


    def demandRoute(self, table_name, key) :
        '''
        @return True on success, False on failure
        '''
        request = Event()
        request.request_id = self.genRequestID()
        request.op = ROUTE_DEMAND
        request.body = (table_name, key)

        response = self.__callMetaServer( request )
        if not response :
            self._logger.error('fail to demand route.')
            return False
        if not response.isOk() :
            self._logger.error('fail to demand route -- %s', response.error)
            return False

        result = response.body

        return True
        ## demandRoute()

    ####-----------------------------------------------------------####

    def lookupTabletServer(self, table_name, key, blocked=True) :
        '''lookup tablet-server

        @param table_name name of table
        @param key key of record
        @param blocked whether blocked if tablet-server not found

        @return list of tablet-server charging (table_name, key); \
                None if not found in non-blocking mode
        '''
        count = 0
        while True :
            tablet_servers = self.__lookupTabletServer(table_name, key) 
            if tablet_servers :
                if len(tablet_servers) < self.__getExpectReplicaNumber(table_name) :
                    # if more tablet-servers are expected, update table_name periodly 
                    if (getTimestamp() - self._route_last_update) > 60000 :
                        self.updateRouteTable()

                return tablet_servers

            # got nothing
            if (not blocked) and (count > 0) :
                return None

            if count == 0 :
                count += 1
            else :
                time.sleep(1.0)
            
            # got nothing 
            # try to alloc route-item
            if not self.demandRoute(table_name, key) :
                continue
            
            # try to update route-table
            if not self.updateRouteTable() :
                continue

            # end of while loop
        ## lookupTabletServer()

    def __lookupTabletServer(self, table_name, key) :
        if table_name not in self._route_tablet :
            return None

        tablets = self._route_tablet[ table_name ]
        index = bisect_left(tablets, (key,key,None,None))
        if index > 0 :
            index -= 1

        key_start,key_stop,ts_list = tablets[ index ]
        if (key_start < key) and (key <= key_stop) :
            return ts_list

        return None
        ## __lookupTabletServer()

    ####-----------------------------------------------------------####

    def genRequestID(self) :
        # TODO: lock
        serial = self._id_serial

        self._id_serial += 1
        if self._id_serial >= self._id_maximum :
            self._id_serial = 1 

        id = '.'.join((str(self._id_base), str(serial)))
        return id
        ## genRequestID()

    def __getExpectReplicaNumber(self, table_name) :
        return  3
        ## __getExpectReplicaNumber()

    ####-----------------------------------------------------------####

    def __callMetaServer(self, request) :
        request.source_queue = self._meta_queue

        # send request
        #self._meta_accessor.getEventQueue().put( request )
        self._meta_accessor.postRequest( request )

        # wait reply
        while True :
            try :
                evt = self._meta_queue.get(True, self._TIMEOUT_WAIT_META_SERVER)
                if evt.response_to == request.request_id :
                    if OP_RESPONSE == evt.op :
                        reply = evt.body
                        self._meta_queue.task_done()
                        break
                    else :
                        response = Response(RC_INTERNAL_ERROR)
                        response.error = evt.body
                        self._meta_queue.task_done()
                        return response
                else :
                    self._meta_queue.task_done()
            except queue.Empty :
                response = Response(RC_TIMEOUT)
                response.error = 'Timeout to wait reply for request "%s"' % str(request)
                return response

        response = Response()
        response.body = reply
        return response
        ## __callMetaServer()


    def __callDataServers(self, request, tablet_servers, timeout_ms=0) :
        '''
        @param request request to be sent
        @param tablet_servers name list of tablet-server
        @param timeout_ms timeout when calling tablet-server

        @return list of (tablet_server_address, response) tuples
        '''
        request.source_queue = self._data_queue

        # send request
        self._data_accessor.postRequest(request, tablet_servers, timeout_ms)

        if timeout_ms > 0 :
            timeout_s = timeout_ms / 1000.0
        else :
            timeout_s = self._TIMEOUT_WAIT_DATA_SERVER

        # wait reply
        replies = []
        while True :
            try :
                evt = self._data_queue.get(True, timeout_s)
                if evt.response_to == request.request_id :
                    reply_from = evt.source_queue
                    reply = evt.body
                    self._data_queue.task_done()
                    replies.append( (reply_from, reply) )

                    if len(replies) >= len(tablet_servers) :
                        # get reply from every tablet-server
                        break
                else :
                    self._data_queue.task_done()
            except queue.Empty :
                if timeout_ms > 0 :
                    # timeout occurs and caller doesnot want to wait
                    break

        return replies
        ## __callDataServers()

    ## class Accessor()



